<?php
/*//. PGID3 GUI Classwork

This software is licensed under the BSD License.

Copyright (c) 2007, Bob Majdak Jr <bob@kateos.org>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

	* Redistributions of source code must retain the above copyright notice,
	this list of conditions and the following disclaimer.

	* Redistributions in binary form must reproduce the above copyright notice,
	this list of conditions and the following disclaimer in the documentation
	and/or other materials provided with the distribution.
      
	* Neither the name of the organization nor the names of its contributors
	may be used to endorse or promote products derived from this software
	without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
.//*/

/*//. bobTextView(void)
  ../ Self contained 'advanced' text display widget. This thing is overkill for
  ../ PGID3 but oh well I like it.
.//*/

class bobTextView extends GtkScrolledWindow {

	const TAG_NORMAL = 1;
	const TAG_STYLE = 2;
	const TAG_URL = 4;

	private $text;

	public function __construct() {
		parent::__construct();
		
		$this->tagdef = array(
			'span' => array('attrib' => self::TAG_STYLE, 'property' => array('style' => 'normal')),
			'url' => array('attrib' => self::TAG_URL, 'property' => array('foreground' => '#0000aa','underline' => Pango::UNDERLINE_SINGLE)),
			'b' => array('attrib' => self::TAG_NORMAL, 'property' => array('weight' => '700')),
			'i' => array('attrib' => self::TAG_NORMAL, 'property' => array('style' => 'italic'))
		);

		$this->view = new GtkTextView;
		$this->buffer = new GtkTextBuffer;

		$this->tag = array();
		$this->table = $this->buffer->get_tag_table();
		
		$this->set_shadow_type(Gtk::SHADOW_ETCHED_IN);
		$this->set_policy(Gtk::POLICY_AUTOMATIC,Gtk::POLICY_AUTOMATIC);

		$this->view->set_wrap_mode(2);
		$this->view->set_editable(0);
		$this->view->set_cursor_visible(0);
		$this->view->set_border_width(2);
		$this->view->set_buffer($this->buffer);
		
		$this->marksig = $this->buffer->connect('mark-set',array($this,'on_mark'));
		
		//. build tags.
		foreach($this->tagdef as $tag => $opt) {
			$this->tag[$tag] = new GtkTextTag($tag);
			
			foreach($opt['property'] as $type => $style) {
				$this->tag[$tag]->set_property($type,$style);
			}
			
			$this->table->add($this->tag[$tag]);
		}
		
		$this->add($this->view);	
		
		$this->show_all();			
		
	}
	
	public function run_browser($url) {
	
		//. need to add more browser support, including windows.
	
		if($browser = trim(`which gnome-open`)) {
			system("{$browser} {$url} &> /dev/null &");
		}
		
		else if($browser = trim(`which firefox`)) {
			system("{$browser} {$url} &> /dev/null &");
		}
		
	}
	
	public function on_mark($w,$i,$m) {
	
		if($m->get_name() != 'insert') { return; }
		
		$pos = $this->buffer->get_insert();
		$iter = $this->buffer->get_iter_at_mark($pos);
//		printf("%d\n",$iter->get_offset());
		foreach($this->tagdef as $id => $tag) {
			if(!preg_match("/^url=?/",$id)) { continue; }
			if($iter->has_tag($this->tag[$id])) {
				foreach($this->urllist as $url) {
					if($url[0] <= $iter->get_offset() && $url[1] >= $iter->get_offset()) {
						$this->run_browser($url[2]);
					}
				}
			}
		}
	
	}
	
	public function set($text) { $this->append($text); }
	
	public function append($input) {
	
		if(!$input) { return; }
	
		$this->urllist = array();	
	
		$this->buffer->disconnect($this->marksig);
		$startchar = array();
	
		$url = 0;
		$cinput = $input;
		foreach($this->tagdef as $tag => $opt) {
			while(preg_match("/<({$tag})((?:=[^>]+)){0,1}>(.*?)<\/\\1>/ms",$input,$tagmatch)) {
			
				//. url list, start position, end position, the url to go to.
				if($tagmatch[1] == 'url') {
					if($tagmatch[2]) {
						$this->urllist[$url] = array(0,0,preg_replace("/^=/","",$tagmatch[2]));
					} else {
						$this->urllist[$url] = array(0,0,$tagmatch[3]);
					}
					
				}
				
				$tagreal = $tagmatch[1];
				$tagstr = $tagmatch[1].$tagmatch[2];
				$tagopt = array();

				if($opt['attrib'] == self::TAG_STYLE && $tagmatch[2]) {
					$tagopt = explode(";",preg_replace("/^\=/","",$tagmatch[2]));
				} else if($opt['attrib'] == self::TAG_URL && $tagmatch[2]) {
					foreach($this->tagdef['url']['property'] as $key => $value) {
						$tagopt[] = $key.":".$value;
					}
				}

				if(($opt['attrib'] & (self::TAG_STYLE | self::TAG_URL)) && !$this->tag[$tagstr]) {

					$this->tagdef[$tagstr] = array('attrib' => self::TAG_STYLE, 'property' => array());
					$this->tag[$tagstr] = new GtkTextTag($tagstr);
					$this->table->add($this->tag[$tagstr]);

					foreach($tagopt as $attrib) {
						list($key,$value) = explode(":",$attrib);
						
						switch($key) {
							case('weight'): { if($value == 'bold') { $value = 700; break; } }
							case('underline'): { $value = (int)$value; }
							case('variant'): { $value = (int)$value; }
						}						
						
						$this->tag[$tagstr]->set_property($key,$value);

						$this->tagdef[$tagstr]['property'][$key] = $value;				
					}

				}
		
				//. start = open tag pos in input + length of current text buffer. 
				//. stop = (close tag pos in input + length of current text buffer) - length of the open tag.
				
				$laststart = $startchar[$tagstr][] = (strpos(utf8_decode($input),"<".$tagstr.">") + strlen(utf8_decode($this->text)));
				$laststop = $stopchar[$tagstr][] = ((strpos(utf8_decode($input),"</".$tagmatch[1].">") - (strlen($tagstr) + 2)) + strlen(utf8_decode($this->text)));		 				
				$lastdist = $chardist[$tagstr][] = ($laststop - $laststart);
						
 				$input = preg_replace("/".preg_quote($tagmatch[0],'/')."/",$tagmatch[3],$input,1);
 				
				//. test for tag realignment after string replacement.
 				foreach($startchar as $elmid => $tagelm) {
 					foreach($tagelm as $id => $pos) {

						//. adjust open tags.
 						if($laststart < $pos) {
 							$startchar[$elmid][$id] -= (strlen($tagstr) + 2);
 						}
  						if($laststop < $pos) {
 							$startchar[$elmid][$id] -= strlen($tagmatch[1]) + 3;
 						}

						//. respace.						
						if($chardist[$elmid][$id] != ($stopchar[$elmid][$id] - $startchar[$elmid][$id])) {
							$makeup = ($stopchar[$elmid][$id] - $startchar[$elmid][$id]) - $chardist[$elmid][$id];
							$stopchar[$elmid][$id] -= $makeup;
						}
						
					}
 				} unset($elemid,$tagelm,$id,$pos);
 				
 				//. realign urls.
				foreach($this->urllist as $item => $info) {
					$dist = $info[1] - $info[0];
					if($laststart < $info[0]) {
						$this->urllist[$item][0] -= (strlen($tagstr) + 2);
					}
					if($laststop < $info[0]) {
						$this->urllist[$item][0] -= (strlen($tagmatch[1]) + 3);
					}
					// space.
					if(($this->urllist[$item][1] - $this->urllist[$item][0]) != $dist) {
						$this->urllist[$item][1] -= ($this->urllist[$item][1] - $this->urllist[$item][0]) - $dist;
					}
 				}

				if(preg_match("/^url=?/",$tagstr)) {
					$this->urllist[$url][0] = $laststart;
					$this->urllist[$url][1] = $laststop;
					++$url;			
				}
				
			}
		}
		
		//. append clean text to buffer.
		$this->text .= $input;		
		$this->buffer->insert_at_cursor($input,strlen($input));

		//. insert formatting marks.
		$b = 0;
		foreach($this->tagdef as $tag => $opt) {
			if(array_key_exists($tag,$startchar)) {
				$countchar = count($startchar[$tag]);
				for($a = 0; $a < $countchar; $a++) {
					$this->buffer->apply_tag(
						$this->tag[$tag],
						$this->buffer->get_iter_at_mark($this->buffer->create_mark($b++, $this->buffer->get_iter_at_offset($startchar[$tag][$a]), FALSE)),
						$this->buffer->get_iter_at_mark($this->buffer->create_mark($b++, $this->buffer->get_iter_at_offset($stopchar[$tag][$a]), FALSE))			
					);
				}
			}
		}

		$this->marksig = $this->buffer->connect('mark-set',array($this,'on_mark'));

	}
	
}

/*//. pgid3MenuBar(pgid3 parent)
  ../ Selfconstructing menubar class.

  ../ no public methods.
.//*/

class pgid3MenuBar extends GtkMenuBar {

	const ROOT = 1;
	const SUBMENU = 2;
	const ITEM = 3;
	const BAR = 4;

	public function __construct($parent) {
		parent::__construct();
		
		$menudef = array(
//			widget => array(type,[parent],label)
			'pgid3' => array(self::ROOT,'PGID3'),
			'open' => array(self::ITEM,'pgid3','Open File...',
				'function' => array($parent,'open_file_dialog')),
			'open-qxmms' => array(self::ITEM,'pgid3','Open XMMS Current',
				'function' => array($parent,'open_file_qxmms')),
			'bar02' => array(self::BAR,'pgid3'),
			'file-info' => array(self::ITEM,'pgid3','View File Info','state' => false,
				'function' => array($parent,'view_file_info')),
			'bar01' => array(self::BAR,'pgid3'),
			'quit' => array(self::ITEM,'pgid3','Quit',
				'function' => array($parent,'quit')),
				
			'help' => array(self::ROOT,'Help'),
			'about' => array(self::ITEM,'help','About PGID3 v'.VERSION,
				'function' => array($parent,'about'))		
		);
		
		$this->menuitem = array();
		
		foreach($menudef as $id => $info) {

			switch($info[0]) {
				case(self::ROOT): {
					$subid = sprintf('%s-gtkmenu',$id);
			
					$this->menuitem[$id] = new GtkMenuItem($info[1]);
					$this->menuitem[$subid] = new GtkMenu;
					$this->menuitem[$id]->set_submenu(
						$this->menuitem[$subid]
					);
					
					$this->add($this->menuitem[$id]);
					break;
				}
				case(self::ITEM): {

					if(!HAS_QXMMS && $id == 'open-qxmms') { break; }				
				
					$menuid = sprintf('%s-gtkmenu',$info[1]);				
				
					$this->menuitem[$id] = new GtkMenuItem($info[2]);
					if(array_key_exists('function',$info)) {
						$this->menuitem[$id]->connect_simple(
							'activate',
							$info['function']						
						);
					}
					
					$this->menuitem[$menuid]->add($this->menuitem[$id]);
					break;
				}
				
				case(self::BAR): {
					$this->menuitem[$id] = new GtkSeparatorMenuItem;
					
					$this->menuitem[$menuid]->add($this->menuitem[$id]);
					break;
				}
			}
		}
		
		$this->show_all();
		
		foreach($menudef as $id => $info) {
			if(array_key_exists('state',$info)) {
				$this->menuitem[$id]->set_sensitive($info['state']);
			}
		}	
		return;		
	}
}

/*//. pgid3StatusBar(bool hasGrip default true)
  ../ Neat little selfcontained statusbar with ease of use.

  ../ public message(string text, bool/int timeout default 0)
  ../     display the message text on the bar, and remove it after timeout
  ../     seconds. if timeout is boolean true, then use default timeout setting.

  ../ public clear(void)
  ../     remove any text being displayed.
.//*/

class pgid3StatusBar extends GtkStatusBar {

	const TIMEOUT = 3;

	private $iid = 0;
	
	public function __construct($grip = true) {
		parent::__construct();
		
		$this->set_has_resize_grip($grip);

		return;
	}
	
	public function message($text,$timeout = 0) {
		$this->pop(0);
		$this->push(0," {$text}");

		if($this->iid) {
			Gtk::timeout_remove($this->iid);
			$this->iid = 0;
		}
		
		if($timeout > 0) {
			if($timeout === true) {
				$this->iid = Gtk::timeout_add(
					(self::TIMEOUT * 1000),
					array($this,'clear')
				);
			} else {
				$this->iid = Gtk::timeout_add(
					($timeout * 1000),
					array($this,'clear')
				);
			}
		}
		
		return;
	}
	
	public function clear() {
		$this->pop(0);
		$this->iid = 0;
		$this->message(sprintf(pgid3::TITLE_STRING,VERSION),0);

		return false;
	}	
}

/*//. pgid3OpenDialog(pgid3 parent)
  ../ A dialog for choosing MP3 files to open from the system.
.//*/

class pgid3OpenDialog extends GtkFileChooserDialog {

	public function __construct($parent) {
		parent::__construct();
	
		$this->set_transient_for($parent);
		$this->set_modal(true);
		$this->set_position(Gtk::WIN_POS_CENTER);		
		$this->set_title('Open MP3 File...');		
		
		$this->open = new GtkButton('Open');
		$this->cancel = new GtkButton('Cancel');
		
		$this->open->set_image(GtkImage::new_from_stock(
			Gtk::STOCK_OPEN,
			Gtk::ICON_SIZE_BUTTON
		)); $this->cancel->set_image(GtkImage::new_from_stock(
			Gtk::STOCK_CANCEL,
			Gtk::ICON_SIZE_BUTTON
		));

		$filter = new GtkFileFilter();
		$filter->set_name('MP3 Files');
		$filter->add_pattern('*.mp3');			
		$this->add_filter($filter);			

		$filter = new GtkFileFilter();
		$filter->set_name('All Files');
		$filter->add_pattern('*');
		$this->add_filter($filter);

		$this->open->connect_simple(
			'clicked',
			array($this,'response'),
			Gtk::RESPONSE_ACCEPT
		); $this->cancel->connect_simple(
			'clicked',
			array($this,'response'),
			Gtk::RESPONSE_CANCEL
		); $this->connect_simple(
			'delete-event',
			array($this,'response'),
			Gtk::RESPONSE_CANCEL
		);
		
		$this->action_area->pack_end($this->cancel);
		$this->action_area->pack_end($this->open);
		
		$this->show_all();
		
		return;
	}

}

/*//. pgid3ErrorDialog(pgid3 parent)
  ../ A dialog for displaying errors.
.//*/

class pgid3ErrorDialog extends GtkDialog {

	public function __construct($title,$message,$parent) {
		parent::__construct();
		
		$this->set_transient_for($parent);
		$this->set_modal(true);
		$this->set_title($title);
		
		$this->hbox = new GtkHBox;
		$this->ibox = new GtkVBox;
		$this->icon = GtkImage::new_from_file(EXECROOT.'/share/pixmaps/pgid3/pgid3_error.png');
		$this->message = new bobTextView;
		$this->ok = new GtkButton('OK');
		
		$this->set_resizable(false);
		$this->set_has_separator(false);
		$this->message->set($message);
		$this->message->set_size_request(250,75);

		$this->connect_simple('delete-event',array($this,'response'),Gtk::RESPONSE_CANCEL);
		$this->ok->connect_simple('clicked',array($this,'response'),Gtk::RESPONSE_CANCEL);
		
		$this->ibox->pack_start($this->icon,false,false,3);
		$this->hbox->pack_start($this->ibox,true,true,3);
		$this->hbox->pack_start($this->message,true,true,3);
		$this->vbox->pack_start($this->hbox,true,true,3);
		$this->action_area->pack_end($this->ok);		
		
		$this->show_all();
		
		return;		
	}

}

/*//. pgid3AboutDialog(pgid3 parent)
  ../ A dialog for displaying errors.
.//*/

class pgid3AboutDialog extends GtkDialog {

	public function __construct($parent) {
		parent::__construct();
		
		$this->set_transient_for($parent);
		$this->set_modal(true);
		$this->set_has_separator(false);
		$this->set_title('About');
		
		$this->ok = new GtkButton('Close');		
		$this->message = new bobTextView;
		$this->label = new GtkLabel;
		$this->abox = new GtkHBox;
		$this->tbox = new GtkVBox;
		$this->ibox = new GtkVBox;
		$this->icon = GtkImage::new_from_file(EXECROOT.'/share/pixmaps/pgid3/pgid3.png');
		
		$this->label->set_markup(sprintf(
			"<span size='xx-large' weight='bold'>PGID3 v%s</span>\n".
			"Bob Majadk Jr &lt;bob@kateos.org&gt;",
			VERSION
		));

		$this->message->set_size_request(350,150);

		$this->message->append("<b>Website:</b>\n\t<url>http://pgid3.sourceforge.net</url>\n\n");

		$this->message->append(
			"<b>Keyboard Shortcuts:</b>\n".
			"\t<b>F1:</b>Toggle ID3v1\n".
			"\t<b>F2:</b>Toggle ID3v2\n".
			"\t<b>F5:</b>Reload current file\n".
			"\t<b>F6:</b>View File Info\n".
			"\t<b>F12:</b>Save tags to file\n".
			"\t<b>CTRL+1:</b> Jump to Artist field\n".
			"\t<b>CTRL+2:</b> Jump to Title field\n".
			"\t<b>CTRL+3:</b> Jump to Album field\n".
			"\t<b>CTRL+4:</b> Jump to Track field\n".
			"\t<b>CTRL+5:</b> Jump to Genre field\n".
			"\t<b>CTRL+6:</b> Jump to Year field\n".
			"\t<b>CTRL+7:</b> Jump to Comment field\n\n"
		);

		if(HAS_QXMMS) {
			$this->message->append(
				"<b>QueryXMMS is installed.</b>\n".
				"\t<b>F7:</b> Goto prev song in XMMS and open it\n".
				"\t<b>F8:</b> Goto next song in XMMS and open it\n".
				"\t<b>F9:</b> Open current XMMS song\n\n"
			);
		} else {
			$this->message->append("QueryXMMS not installed. See <url>http://qxmms.sourceforge.net</url> to get it.\n\n");
		}	
		
		$this->message->append(
			'This application is released under the BSD License. If you did '.
			'not recieve a copy of this license with this application then '.
			'you may aquire one from the website.'
		);

		$this->connect_simple(
			'delete-event',
			array($this,'response'),
			Gtk::RESPONSE_CANCEL
		); $this->ok->connect_simple(
			'clicked',
			array($this,'response'),
			Gtk::RESPONSE_CANCEL
		);

		$this->ibox->pack_start($this->icon,false,false,3);
		$this->tbox->pack_start($this->label,false,false,3);
		$this->tbox->pack_start($this->message,false,false,3);
		$this->abox->pack_start($this->ibox,false,false,3);
		$this->abox->pack_start($this->tbox,true,true,3);
		$this->vbox->pack_start($this->abox,true,true,3);
		$this->action_area->pack_start($this->ok);
		$this->show_all();

		return;	
	}
}

/*//. pgid3FileInfoDialog(pgid3 parent, string filename)
  ../ A dialog for displaying errors.
.//*/

class pgid3FileInfoDialog extends GtkDialog {

	public function __construct($parent,$filename) {
		parent::__construct();
		
		$this->set_transient_for($parent);
		$this->set_modal(true);
		$this->set_has_separator(false);
		$this->set_title('File Info');
		$this->set_resizable(false);

		$entry = array('File','Bitrate','Sample Rate','Length','Size');

		$this->ok = new GtkButton('Close');
		$this->infobox = new GtkVBox;
		$this->ibox = new GtkVBox;
		$this->abox = new GtkHBox;
		$this->icon = GtkImage::new_from_file(EXECROOT.'/share/pixmaps/pgid3/pgid3.png');

		$this->entry = array();
		$this->label = array();
		$this->lalign = array();
		$this->hbox = array();
		foreach($entry as $id) {
			$this->hbox[$id] = new GtkHBox;
			$this->lalign[$id] = new GtkAlignment(J_RIGHT, J_MIDDLE, 0, 0);
			$this->label[$id] = new GtkLabel($id.": ");
			$this->lalign[$id]->set_size_request(75,pending);
			$this->entry[$id] = new GtkEntry;
			$this->entry[$id]->set_editable(false);
			
			$this->lalign[$id]->add($this->label[$id]);
			$this->hbox[$id]->pack_start($this->lalign[$id],false,false,3);
			$this->hbox[$id]->pack_end($this->entry[$id],true,true,3);
			$this->infobox->pack_start($this->hbox[$id],true,true,3);
		}

		$fileinfo = pgid3_file_info($filename);

		$this->entry['File']->set_text($filename);
		$this->entry['Bitrate']->set_text($fileinfo['bitrate'].' Kbps');
		$this->entry['Sample Rate']->set_text($fileinfo['samplerate'].' KHz');
		$this->entry['Length']->set_text($fileinfo['length']);
		$this->entry['Size']->set_text($fileinfo['size']);

		$this->connect_simple(
			'delete-event',
			array($this,'response'),
			Gtk::RESPONSE_CANCEL
		); $this->ok->connect_simple(
			'clicked',
			array($this,'response'),
			Gtk::RESPONSE_CANCEL
		);
	
		$this->ibox->pack_start($this->icon,false,false,3);
		$this->abox->pack_start($this->ibox,false,false,3);
		$this->abox->pack_start($this->infobox,true,true,3);
		$this->vbox->pack_start($this->abox,true,true,3);
		$this->action_area->pack_start($this->ok);
	
		$this->show_all();
		return;
	}

}

/*//. pgid3(void)
  ../ Selfconstructing application class...
.//*/

class pgid3 extends GtkWindow {

	const TITLE_STRING = 'PGID3 v%s';
	
	private $curfile;
	private $lastdir;

	public function __construct() {
		parent::__construct();

		$this->vbox = new GtkVBox;
		$this->menu = new pgid3MenuBar($this);
		$this->status = new pgid3StatusBar(false);
		$this->hboxdef = array(
			'artist' => array(),
			'title' => array(),
			'album' => array(),
			'genre' => array(),
			'comment' => array(),
			'options' => array(),
			'write' => array('pack' => (P_EXPAND | P_FILL)),
			'open' => array(),
			'qxmms' => array(),
			'strip' => array()
		);
		
		$this->widgetdef = array(
		//	widget => array(type,hbox,label),
			'artist' => array(T_ENTRY,'artist','Artist','width' => 200,'state' => false),
			'title' => array(T_ENTRY,'title','Title','width' => 200,'state' => false),
			'album' => array(T_ENTRY,'album','Album','width' => 100,'state' => false),
			'track' => array(T_ENTRY,'album','Track','width' => 50,'labelwidth' => 39,'state' => false,'expand' => false),
			'genre' => array(T_DROPDOWN_ENTRY,'genre','Genre','width' => 100,'state' => false),
			'year' => array(T_ENTRY,'genre','Year','width' => 50,'labelwidth' => 39,'state' => false,'expand' => false),
			'comment' => array(T_ENTRY,'comment','Comment','width' => 200,'state' => false),
			'writev1' => array(T_CHECKBOX,'options','Write ID3v1','state' => false),
			'writev2' => array(T_CHECKBOX,'options','Write ID3v2','state' => false),
			'write' => array(T_BUTTON,'write','Write Tags','width' => 200,'state' => false,'function' => array($this,'write_file')),
			'open' => array(T_BUTTON,'open','Open File','width' => 200,'function' => array($this,'open_file_dialog')),
			'qxmms' => array(T_BUTTON,'qxmms','Open Current (XMMS)','width' => 200,'function' => array($this,'open_file_qxmms')),
			'stripv1' => array(T_BUTTON,'strip','Strip ID3v1','width' => 200,'state' => false,'function' => array($this,'strip_file'),'data' => 1),
			'stripv2' => array(T_BUTTON,'strip','Strip ID3v2','width' => 200,'state' => false,'function' => array($this,'strip_file'),'data' => 2)
		); $this->widget = array();
		
		$this->hbox = array();
		foreach($this->hboxdef as $id => $widget) {
			$this->hbox[$id] = new GtkHBox;
		}

		foreach($this->widgetdef as $id => $info) {
			
			$label = sprintf('%s-gtklabel',$id);
			$lalign = sprintf('%s-gtkalignment',$label);	
		
			switch($info[0]) {
				case(T_ENTRY): {
					$this->widget[$id] = new GtkEntry;
					$this->widget[$label] = new GtkLabel($info[2]);
					$this->widget[$lalign] = new GtkAlignment(J_RIGHT, J_MIDDLE, 0, 0);

					$this->widget[$lalign]->set_size_request((
						(array_key_exists('labelwidth',$info))?
							($info['labelwidth']):
							(60)
					),pending);
					$this->widget[$id]->set_size_request($info['width'],pending);
					
					$this->widget[$lalign]->add($this->widget[$label]);					
					$this->hbox[$info[1]]->pack_start($this->widget[$lalign],false,false,3);
					$this->hbox[$info[1]]->pack_start($this->widget[$id],((array_key_exists('expand',$info))?($info['expand']):(true)),true,3);				

					if(array_key_exists('state',$info)) {
						$this->widget[$id]->set_sensitive($info['state']);
					}

					break;
				}
				
				case(T_DROPDOWN_ENTRY): {
					$this->widget[$id] = GtkComboBoxEntry::new_text();
					$this->widget[$label] = new GtkLabel($info[2]);
					$this->widget[$lalign] = new GtkAlignment(1.0, 0.5, 0, 0);

					$this->widget[$lalign]->set_size_request(60,pending);
					$this->widget[$id]->set_size_request($info['width'],pending);
					
					$this->widget[$lalign]->add($this->widget[$label]);					
					$this->hbox[$info[1]]->pack_start($this->widget[$lalign],false,false,3);
					$this->hbox[$info[1]]->pack_start($this->widget[$id],true,true,3);

					if(array_key_exists('state',$info)) {
						$this->widget[$id]->set_sensitive($info['state']);
					}

					break;
				}
				
				case(T_CHECKBOX): {
					$this->widget[$id] = new GtkCheckButton($info[2]);
					$this->hbox[$info[1]]->pack_start($this->widget[$id],true,true,3);

					if(array_key_exists('state',$info)) {
						$this->widget[$id]->set_sensitive($info['state']);
					}

					break;				
				}
				
				case(T_BUTTON): {
					$this->widget[$id] = new GtkButton($info[2]);
					$this->hbox[$info[1]]->pack_start($this->widget[$id],true,true,3);

					if(array_key_exists('function',$info)) {
						if(array_key_exists('data',$info)) {
							$this->widget[$id]->connect_simple('clicked',$info['function'],$info['data']);
						} else {
							$this->widget[$id]->connect_simple('clicked',$info['function']);
						}
					}		

					if(array_key_exists('state',$info)) {
						$this->widget[$id]->set_sensitive($info['state']);
					}
					
					break;				
				}
			}
		}

		$this->set_title(sprintf(self::TITLE_STRING,VERSION));
		$this->set_position(Gtk::WIN_POS_CENTER);
		$this->set_icon_from_file(EXECROOT."/share/pixmaps/pgid3/pgid3.png");
		
		$this->status->message(sprintf(self::TITLE_STRING,VERSION),true);
		
		//. setup drop down menu...
		$model = $this->widget['genre']->get_model();
		foreach($GLOBALS['genre-list'] as &$value) {
			$model->append(array($value));
		}
		
		//. ... and enable autocomplete.
		$autocomp = new GtkEntryCompletion();
		$autocomp->set_model($model);
		$autocomp->set_text_column(0);
		$this->widget['genre']->child->set_completion($autocomp);
		
		//. and i think the write button looks kickass fatter than the others.
		$this->widget['write']->child->set_padding(2,12);
		
		//. setup keybindings.
		$this->connect('key-press-event',array($this,'on_key_press'));
				
		//. setup drag and drop.
		$this->vbox->drag_dest_set(
			Gtk::DEST_DEFAULT_ALL,
			array(array('text/uri-list',0,0)),
			(Gdk::ACTION_COPY | Gdk::ACTION_MOVE)
		); $this->vbox->connect(
			"drag-data-received",
			array($this,'on_recieve_drop')
		);
		
		//. when i connected to the window directly, i was getting multiple
		//. copies per drop. not sure why at the moment but drag/drop on the
		//. main vbox seems to work fine.

		$this->connect_simple('delete-event',array($this,'quit'));

		$this->vbox->pack_start($this->menu,false,false,0);

		foreach($this->hboxdef as $id => $info) {
			if($id == 'qxmms') {
				if(HAS_QXMMS == true) {
					$this->vbox->pack_start(
						$this->hbox[$id],
						((array_key_exists('pack',$info) && $info['pack'] & P_EXPAND)?(true):(false)),
						((array_key_exists('pack',$info) && $info['pack'] & P_FILL)?(true):(false)),
						3
					);
				}
			} else {
				$this->vbox->pack_start(
					$this->hbox[$id],
					((array_key_exists('pack',$info) && $info['pack'] & P_EXPAND)?(true):(false)),
					((array_key_exists('pack',$info) && $info['pack'] & P_FILL)?(true):(false)),
					3
				);
			}
		}
		
		$this->vbox->pack_start(
			$this->status,
			false,false,
			0
		);
	
		$this->add($this->vbox);
		$this->show_all();
		
		return;		
	}
	
	//. do something on drag and drop.
	public function on_recieve_drop($widget,$context,$x,$y,$data,$info,$time) {
		$entry = explode("\n",$data->data);
		
		if(UNIX) {
			$entry[0] = preg_replace(
				'/^file:\/{1,2}(?:localhost)?/',
				'',
				urldecode(trim($entry[0]))
			);
		} else {
			$entry[0] = preg_replace(
				'/^file:\/\/\//',
				'',
				urldecode(trim($entry[0]))
			);		
		}
	
		if(is_file($entry[0])) {
			$this->open_file($entry[0]);
		}
		
		return;
	}
	
	//. do something on key press.
	public function on_key_press($widget,$e) {
	
		if($e->keyval >= Gdk::KEY_0 && $e->keyval <= Gdk::KEY_9 && $e->state & Gdk::CONTROL_MASK) { //. 1 to 0 (number row).
			$field = array(
				Gdk::KEY_1 => 'artist',
				Gdk::KEY_2 => 'title',
				Gdk::KEY_3 => 'album',
				Gdk::KEY_4 => 'track',
				Gdk::KEY_5 => 'genre',
				Gdk::KEY_6 => 'year',
				Gdk::KEY_7 => 'comment'
			);
			if($field[$e->keyval] == "genre") {
				$this->set_focus($this->widget[$field[$e->keyval]]->child);
			} else {
				$this->set_focus($this->widget[$field[$e->keyval]]);
			}
			$this->activate_focus();
			return 0;
		}
	
		switch($e->keyval) {
	//		case(Gdk::KEY_Escape): {
	//			$this->quit();
	//			break;
	//		}
			case(Gdk::KEY_q): {
				if($e->state & Gdk::CONTROL_MASK) {
					pgid3_quit();
					break;
				}
				break;
			}
			case(Gdk::KEY_F1): {
				$this->widget['writev1']->set_active(!((bool)$this->widget['writev1']->get_active()));
				break;
			}
			case(Gdk::KEY_F2): {
				$this->widget['writev2']->set_active(!((bool)$this->widget['writev2']->get_active()));
				break;
			}
			case(Gdk::KEY_F5): {
				$this->open_file($this->curfile);
				break;
			}
			case(Gdk::KEY_F6): {
				$this->view_file_info();
				break;
			}
			case(Gdk::KEY_F12): {
				$this->write_file();
				break;
			}
		}
	
		if(HAS_QXMMS) {
			switch($e->keyval) {
				case(Gdk::KEY_F7): { //. F7
					pgid3_qxmms("-t prev");
					$this->open_file_qxmms();
					break;
				}
				case(Gdk::KEY_F8): { //. F8
					pgid3_qxmms("-t next");
					$this->open_file_qxmms();
					break;
				}
				case(Gdk::KEY_F9): { //. F9
					$this->open_file_qxmms();
					break;
				}
			}
		}

		return;
	}
	
	//. set the fields with the tag data.
	public function set_field_data($data) {

		foreach($GLOBALS['valid-id3-type'] as $id3 => $type) {
			if(array_key_exists($id3,$data)) {
				if($id3 == 'genre') {				
					$this->widget[$id3]->child->set_text($data[$id3]);
				} else {
					$this->widget[$id3]->set_text($data[$id3]);
				}
			}
			
			else {
				if($id3 == 'genre') {				
					$this->widget[$id3]->child->set_text('');
				} else {
					$this->widget[$id3]->set_text('');
				}			
			}
		}
		
		if($data['v1']) {
			$this->widget['writev1']->set_active(true);
			$this->widget['stripv1']->set_sensitive(true);
		} else {
			$this->widget['writev1']->set_active(false);
			$this->widget['stripv1']->set_sensitive(false);
		}

		if($data['v2']) {
			$this->widget['writev2']->set_active(true);
			$this->widget['stripv2']->set_sensitive(true);
		} else {
			$this->widget['writev2']->set_active(false);
			$this->widget['stripv2']->set_sensitive(false);
		}		
		return;
	}

	//. choose which file to open via QueryXMMS.
	public function open_file_qxmms() {

		$filename = trim(`qxmms -f`);
		if(!is_file($filename)) {
			$this->status->message(sprintf('Unable to open %s',basename($filename)),true);
			return;
		} else {
			$this->status->message(sprintf('File: %s',basename($filename)),true);
			$this->open_file($filename);
		}

		return;
	}
	
	//. choose which file to open via a file chooser dialog.
	public function open_file_dialog() {
		
		$this->set_sensitive(false);
		$dialog = new pgid3OpenDialog($this);
		if($this->lastdir) { $dialog->set_current_folder($this->lastdir); }
		$action = $dialog->run();
		$dialog->hide();

		if($action == Gtk::RESPONSE_ACCEPT) {
			if($filename = $dialog->get_filename()) {
				$this->open_file($filename);
				$this->status->message(sprintf('File: %s',basename($filename)),true);
			}		
		}
		
		$dialog->destroy();
		unset($dialog);
		$this->set_sensitive(true);
		
		return;
	}

	//. the final action after choosing a file, perform the open operation.
	public function open_file($filename) {
		
		if(!is_file($filename)) {
			$this->status->message(sprintf('Unable to open %s',basename($filename)),true);
			return false;
		}
		
		$tag = pgid3_get_tags($filename);
		$this->set_field_data($tag);
		
		$this->menu->menuitem['file-info']->set_sensitive(true);
		$this->widget['write']->set_sensitive(true);
		$this->widget['writev1']->set_sensitive(true);
		$this->widget['writev2']->set_sensitive(true);
		foreach($GLOBALS['valid-id3-type'] as $id3 => $type) {
			$this->widget[$id3]->set_sensitive(true);	
		}
		
//		if($this->curfile == $filename) {
//			$this->status->message('File Reloaded');
//		}

		$this->curfile = $filename;
		$this->lastdir = dirname($filename);
		
		return true;
	}
	
	//. save the tags to disk.
	public function write_file() {

		if(!is_file($this->curfile)) { return false; }
		if(!is_writable($this->curfile)) {
			$this->status->message('Unable to write to file.');
			return false;
		}
		
		$data = array();
		foreach($GLOBALS['valid-id3-type'] as $id3 => $type) {
			if($id3 == 'genre') {
				$data[$id3] = $this->widget[$id3]->child->get_text();
			} else {
				$data[$id3] = $this->widget[$id3]->get_text();
			}
		}
		
		$data['v1'] = $this->widget['writev1']->get_active();
		$data['v2'] = $this->widget['writev2']->get_active();
		
		if(!$data['v1'] && !$data['v2']) {

			$this->set_sensitive(false);
			$dialog = new pgid3ErrorDialog(
				'PGID3 Error',
				'You must select at least one of the versions of tags to write, ID3v1 or ID3v2.',
				$this
			);
			$dialog->run();
			$dialog->hide();
			$dialog->destroy(); unset($dialog);
			$this->set_sensitive(true);
			
			return false;
		
		} else {
			$result = pgid3_write_tags($this->curfile,$data);

			if($result < 0) {
			
			} else if($result === true) {
				$this->open_file($this->curfile);
				
				if($data['v1'] && $data['v2']) {
					$this->status->message('ID3v1 and ID3v2 tags saved.',true);
				} else if($data['v1']) {
					$this->status->message('ID3v1 tags saved.',true);
				} else if($data['v2']) {
					$this->status->message('ID3v2 tags saved.',true);
				}
			}
		}	
		
		return true;
	}
	
	//. remove tags from the file.
	public function strip_file($type) {
		
		switch($type) {
			case(1): {
				pgid3_strip_id3v1($this->curfile);
				$this->widget['writev1']->set_active(false);
				$this->widget['stripv1']->set_sensitive(false);
				$this->status->message('ID3v1 tags removed.',true);
				break;
			}
			case(2): {
				pgid3_strip_id3v2($this->curfile);
				$this->widget['writev2']->set_active(false);
				$this->widget['stripv2']->set_sensitive(false);
				$this->status->message('ID3v2 tags removed.',true);
				break;
			}
		}
		
		$this->open_file($this->curfile);
	
		return;
	}
	
	//. view file info dialog.
	public function view_file_info() {
	
		if(!is_file($this->curfile)) { return; }
	
		$this->set_sensitive(false);
		$dialog = new pgid3FileInfoDialog($this,$this->curfile);
		$dialog->run();
		$dialog->hide();
		$dialog->destroy(); unset($dialog);
		$this->set_sensitive(true);
	
		return;
	}
	
	//. view about dialog.
	public function about() {

		$this->set_sensitive(false);
		$dialog = new pgid3AboutDialog($this);
		$dialog->run();
		$dialog->hide();
		$dialog->destroy(); unset($dialog);
		$this->set_sensitive(true);
		
		return;		
	}


	//. cause the application to quit.
	public function quit() {
		Gtk::main_quit();
		
		return;
	}
	
}

?>